package heven.holt.library.http.cookie

import android.content.Context
import android.text.TextUtils
import heven.holt.library.extensions.byteArrayToHexString
import heven.holt.library.extensions.hexStringToByteArray
import okhttp3.Cookie
import okhttp3.HttpUrl
import okio.IOException
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.concurrent.ConcurrentHashMap


class PersistentCookieStore(
    context: Context
) : CookieStore {
    companion object {
        private const val COOKIE_PREFS = "CookiePrefsFile"
        private const val HOST_NAME_PREFIX = "host_"
        private const val COOKIE_NAME_PREFIX = "cookie_"
    }

    private val cookies: MutableMap<String, ConcurrentHashMap<String, Cookie>> = mutableMapOf()
    private val cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, Context.MODE_PRIVATE)

    init {
        for ((key, value) in cookiePrefs.all) {
            if (key !is String || !key.contains(HOST_NAME_PREFIX)) continue
            if (value !is String || value.isEmpty()) continue
            if (!cookies.containsKey(key)) {
                cookies[key] = ConcurrentHashMap()
            }
            val cookieNameArr: List<String> = value.split(",")
            for (name in cookieNameArr) {
                val encodedCookie =
                    cookiePrefs.getString("$COOKIE_NAME_PREFIX$name", null) ?: continue
                val decodeCookie = decodeCookie(encodedCookie)
                decodeCookie?.let { cookies[key]!!.put(name, decodeCookie) }
            }
        }

        clearExpired()
    }

    /**
     * 移除失效的cookie
     */
    private fun clearExpired() {
        val prefsWriter = cookiePrefs.edit()

        for ((key, value) in cookies) {
            var changeFlag = false

            for ((name, cookie) in value) {
                if (cookie.isCookieExpired()) {
                    value.remove(name)
                    prefsWriter.remove("$COOKIE_NAME_PREFIX$name")
                    changeFlag = true
                }
            }

            if (changeFlag) {
                prefsWriter.putString(key, TextUtils.join(",", cookies.keys))
            }
        }

        prefsWriter.apply()
    }

    override fun add(httpUrl: HttpUrl, cookie: Cookie) {
        if (!cookie.persistent) return
        val name = cookie.cookieName()
        val hostKey = httpUrl.hostName()

        if (!cookies.containsKey(hostKey)) {
            cookies[hostKey] = ConcurrentHashMap()
        }
        cookies[hostKey]!![name] = cookie

        val prefsWriter = cookiePrefs.edit()
        prefsWriter.putString(hostKey, TextUtils.join(",", cookies[hostKey]!!.keys))
        prefsWriter.putString(COOKIE_NAME_PREFIX + name, SerializableCookie(cookie).encodeCookie())
        prefsWriter.apply()
    }

    override fun add(httpUrl: HttpUrl, cookies: List<Cookie>) {
        for (cookie in cookies) {
            if (cookie.isCookieExpired()) continue
            this.add(httpUrl, cookie)
        }
    }

    override fun get(httpUrl: HttpUrl): List<Cookie> {
        return this.get(httpUrl.hostName())
    }

    override fun getCookies(): List<Cookie> {
        val result = mutableListOf<Cookie>()
        for (hostKey in cookies.keys) {
            result.addAll(this.get(hostKey))
        }
        return result
    }

    private fun get(hostKey: String): List<Cookie> {
        val result = mutableListOf<Cookie>()
        if (cookies.containsKey(hostKey)) {
            for ((_, cookie) in cookies[hostKey]!!) {
                if (cookie.isCookieExpired()) {
                    this.remove(hostKey, cookie)
                } else {
                    result.add(cookie)
                }
            }
        }
        return result
    }

    override fun remove(httpUrl: HttpUrl, cookie: Cookie): Boolean {
        return this.remove(httpUrl.hostName(), cookie)
    }

    private fun remove(hostKey: String, cookie: Cookie): Boolean {
        val name = cookie.cookieName()
        if (cookies.containsKey(hostKey) && cookies[hostKey]!!.containsKey(name)) {
            cookies[hostKey]!!.remove(name)

            val prefsWriter = cookiePrefs.edit()
            prefsWriter.remove(COOKIE_NAME_PREFIX + name)
            prefsWriter.putString(hostKey, TextUtils.join(",", cookies[hostKey]!!.keys))
            prefsWriter.apply()
            return true
        }
        return false
    }

    override fun removeAll(): Boolean {
        val prefsWriter = cookiePrefs.edit()
        prefsWriter.clear()
        prefsWriter.apply()
        cookies.clear()
        return true
    }

    private fun Cookie.cookieName(): String = this.name + this.domain

    private fun HttpUrl.hostName(): String =
        if (this.host.startsWith(HOST_NAME_PREFIX)) this.host else HOST_NAME_PREFIX + this.host

    /**
     * 判断cookie是否失效
     */
    private fun Cookie.isCookieExpired(): Boolean = this.expiresAt < System.currentTimeMillis()

    private fun decodeCookie(cookieString: String): Cookie? {
        val bytes = cookieString.hexStringToByteArray()
        val byteArrayInputStream = ByteArrayInputStream(bytes)
        var cookie: Cookie? = null
        try {
            val objectInputStream = ObjectInputStream(byteArrayInputStream)
            cookie = (objectInputStream.readObject() as SerializableCookie).getCookie()
        } catch (e: IOException) {
            Timber.e(e, "IOException in decodeCookie")
        } catch (e: ClassNotFoundException) {
            Timber.e(e, "ClassNotFoundException in decodeCookie")
        } finally {
            byteArrayInputStream.close()
        }
        return cookie
    }

    private fun SerializableCookie.encodeCookie(): String? {
        val os = ByteArrayOutputStream()
        try {
            val outputStream = ObjectOutputStream(os)
            outputStream.writeObject(this)
        } catch (e: IOException) {
            Timber.e(e, "IOException in encodeCookie")
            return null
        }
        return os.use {
            it.toByteArray().byteArrayToHexString()
        }
    }
}